Conversation
📝 WalkthroughПояснениеЭта PR интегрирует функциональность Art-TTS (синтез текста в речь) в игру. Добавляет выбор голоса в редактор профиля и маски голоса, реализует серверную генерацию TTS с кэшированием и ограничением скорости, добавляет клиентскую систему воспроизведения TTS и расширяет системы радио и речи для поддержки синтеза голоса. Включает поддержку русского языка и каталог голосовых прототипов. Изменения
Диаграммы последовательностиsequenceDiagram
actor User as Игрок
participant UI as HumanoidProfileEditor UI
participant Editor as HumanoidProfileEditor
participant Profile as HumanoidCharacterProfile
participant Server as Сервер
participant DB as База данных
User->>UI: Выбирает голос из VoiceButton
UI->>Editor: SetVoice(newVoice)
Editor->>Profile: WithVoice(newVoice)
Editor->>Editor: IsDirty = true
Editor->>UI: UpdateTTSVoicesControls()
UI->>UI: Обновляет выбранный голос
User->>Server: Сохраняет профиль
Server->>Server: ConvertProfiles(profile)
Server->>DB: Сохраняет profile.Voice
DB-->>Server: OK
Server-->>User: Профиль сохранён
sequenceDiagram
actor User as Игрок
participant UI as VoicePlayButton
participant Editor as HumanoidProfileEditor
participant TTSClient as TTSSystem (Client)
participant Server as TTSSystem (Server)
participant TTSMgr as TTSManager
participant API as TTS API
User->>UI: Нажимает кнопку предпросмотра
UI->>Editor: PlayPreviewTTS()
Editor->>TTSClient: RequestPreviewTTS(voiceId)
TTSClient->>Server: RequestPreviewTTSEvent
Server->>Server: Генерирует случайный текст
Server->>Server: Проверяет ограничения скорости
Server->>TTSMgr: ConvertTextToSpeech(speaker, text)
TTSMgr->>TTSMgr: Проверяет кэш
alt Кэш попадание
TTSMgr-->>TTSMgr: Возвращает кэшированное аудио
else Кэш промах
TTSMgr->>API: HTTP GET с текстом и голосом
API-->>TTSMgr: WAV аудио
TTSMgr->>TTSMgr: Сохраняет в кэш
end
TTSMgr-->>Server: byte[] WAV данные
Server->>TTSClient: PlayTTSEvent(data, sourceUid)
TTSClient->>TTSClient: LoadAudioWav(data)
TTSClient->>TTSClient: Воспроизведение звука
User-->>User: Слышит голос
sequenceDiagram
actor User as Отправитель
participant Speech as Система речи
participant Radio as RadioSystem
participant TTS as TTSSystem (Server)
participant TTSMgr as TTSManager
participant Audio as TTSSystem (Client)
User->>Speech: Отправляет сообщение по радио
Speech->>Radio: SendRadioMessage(uid, message, voice)
Radio->>Radio: Извлекает voice из TTSComponent
Radio->>Radio: RadioReceiveEvent(message, voice, ...)
Radio->>TTS: TTSRadioPlayEvent(message, voice, effect="radio")
TTS->>TTS: Генерирует TTS для радио эффекта
TTS->>TTSMgr: ConvertTextToSpeech(speaker, sanitized_text, "radio")
TTSMgr->>TTSMgr: Проверяет кэш, обрабатывает запрос
TTSMgr-->>TTS: byte[] WAV аудио
TTS->>Audio: PlayTTSEvent(broadcast)
Audio->>Audio: Воспроизводит с эффектом радио
User-->>User: Слышит синтезированное сообщение
Оценка усилий при проверке кода🎯 4 (Complex) | ⏱️ ~60 minutes Предлагаемые метки
Стихотворение
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (7)
Content.Shared/_Art/CVars/ArtCVars.cs-17-21 (1)
17-21:⚠️ Potential issue | 🟡 MinorНеверный XML-комментарий (копипаст).
Комментарий "URL of the TTS server API" не соответствует переменной
TTSClientEnabled.📝 Предлагаемое исправление
/// <summary> -/// URL of the TTS server API. +/// Whether TTS is enabled on the client. /// </summary> public static readonly CVarDef<bool> TTSClientEnabled =🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/_Art/CVars/ArtCVars.cs` around lines 17 - 21, Комментарий XML над объявлением public static readonly CVarDef<bool> TTSClientEnabled некорректен (копипаст — "URL of the TTS server API"); замените его на корректное описание состояния этой булевой переменной, например кратко поясняющее, что переменная включает/отключает TTS-клиент (например: "Enable/disable the TTS client" или на русском "Включает/выключает TTS-клиент"), сохранив формат <summary>... </summary> над TTSClientEnabled и не меняя саму сигнатуру CVarDef<bool>.Resources/_Art/TTS/tts-voices.yml-93-112 (1)
93-112:⚠️ Potential issue | 🟡 MinorУточнение о статусе голосов в TTS-системе.
Файл содержит идентификаторы синтетических TTS-голосов, а не записанные голоса реальных людей. TTS-система генерирует речь из текста, а не воспроизводит подготовленные аудиофайлы. Хотя включение имён реальных политиков (Байден, Обама, Трамп) среди голосов потенциально может создать правовые вопросы относительно прав на персональность и использование имён известных личностей, контекст некоммерческого проекта (согласно LICENSE.TXT) и игровая среда значительно снижают этот риск. Рекомендуется добавить комментарий в код для уточнения парody/развлекательного характера этих голосовых идентификаторов.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/_Art/TTS/tts-voices.yml` around lines 93 - 112, The YAML currently lists synthetic TTS voice identifiers for "Джо Байден" (speaker/id: biden), "Барак Обама" (speaker/id: obama) and "Дональд Трамп" (speaker/id: trump) without clarifying they are synthetic/parody; add a brief comment near these entries (or at the top of Resources/_Art/TTS/tts-voices.yml) stating that these are synthetic TTS voices generated from text (not recorded audio), intended for parody/entertainment in a non-commercial/game context and not representations of real recorded performances, to reduce legal ambiguity and clarify intent.Content.Client/Options/UI/Tabs/AudioTab.xaml-11-11 (1)
11-11:⚠️ Potential issue | 🟡 MinorНарушение паттерна локализации.
Строки
"Громкость TTS:"и"Включить ТТС"жёстко закодированы на русском языке, тогда как остальные элементы используют{Loc '...'}для поддержки локализации. Это нарушает консистентность и затрудняет перевод интерфейса.🌐 Предлагаемое исправление
-<ui:OptionSlider Name="SliderVolumeTts" Title="Громкость TTS:"/> <!-- Art-TTS --> +<ui:OptionSlider Name="SliderVolumeTts" Title="{Loc 'ui-options-tts-volume'}"/> <!-- Art-TTS -->-<CheckBox Name="TtsClientCheckBox" Text="Включить ТТС"/> <!-- Art-TTS --> +<CheckBox Name="TtsClientCheckBox" Text="{Loc 'ui-options-tts-enabled'}"/> <!-- Art-TTS -->Не забудьте добавить соответствующие ключи в файлы локализации.
Also applies to: 19-19
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/Options/UI/Tabs/AudioTab.xaml` at line 11, В XAML убери жёстко закодированные русские строки и замени их на локализуемые ресурсы, используя шаблон {Loc 'KeyName'} для элементов SliderVolumeTts (Title) и соответствующего чекбокса/элемента "Включить ТТС" (примерно на строке с именем, содержащим Tts или EnableTts); добавь уникальные ключи (например Audio.VolumeTts и Audio.EnableTts) в файлы локализации и обнови ресурсы перевода, чтобы сохранить консистентность с остальными UI-элементами.Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs-90-92 (1)
90-92:⚠️ Potential issue | 🟡 MinorСбрасывайте выбор, если голос больше не доступен.
Если
voiceне найден в_voices, окно оставляет прошлыйVoiceSelectorкак есть. При повторном открытии окна или смене списка голосов UI начнёт показывать устаревший выбор, который уже не соответствует серверному состоянию. Здесь нужен явный reset/fallback перед поиском.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs` around lines 90 - 92, Reset the selector before attempting to re-select the voice so stale UI state is cleared: set the VoiceSelector selection to an empty/fallback state (e.g., SelectedIndex = -1 or clear selection) prior to calling _voices.FindIndex(...), then if voiceIdx != -1 call VoiceSelector.Select(voiceIdx); ensure you reference the existing variables/methods (_voices, voiceIdx, VoiceSelector.Select) and add the explicit reset when the voice is not found so the UI shows a cleared/fallback choice instead of the previous selection.Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs-196-200 (1)
196-200:⚠️ Potential issue | 🟡 MinorДобавьте guard для
UpdateTTSVoicesControls()или сделайте её безопасным no-op при выключенном TTS.
InitializeVoice()вызывается только подArtCVars.TTSClientEnabled(строка 199), ноUpdateTTSVoicesControls()вызывается без этого guard вSetProfile()(строка 300) иOnReset()(строка 380). Когда TTS отключён,_voiceListостаётся пустым, иUpdateTTSVoicesControls()очищаетVoiceButton, но не заполняет его — результат: неконсистентное состояние UI (элемент управления существует, но всегда пуст). Либо добавьте защиту перед вызовами, либо убедитесь, что метод имеет ранний выход при отключённом TTS и не опирается на состояние изInitializeVoice().🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs` around lines 196 - 200, The UpdateTTSVoicesControls method can be called when TTS is disabled which leaves _voiceList empty and yields an inconsistent UI; either add a guard before calling it in SetProfile and OnReset (check configurationManager.GetCVar(ArtCVars.TTSClientEnabled) or TTSContainer.Visible) or change UpdateTTSVoicesControls to be a safe no-op when TTS is off (early return if TTS disabled or if _voiceList is null/empty) so it does not rely on InitializeVoice having run; update references to InitializeVoice, SetProfile, OnReset, _voiceList, and VoiceButton accordingly.Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs-50-55 (1)
50-55:⚠️ Potential issue | 🟡 MinorПотенциальный
IndexOutOfRangeExceptionпри пустом списке голосов.Если
_voiceListпуст, вызов_voiceList[firstVoiceChoiceId](или_voiceList[0]после упрощения) приведёт к исключению.🛡️ Предлагаемое исправление с проверкой
var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice); - if (!VoiceButton.TrySelectId(voiceChoiceId) && - VoiceButton.TrySelectId(firstVoiceChoiceId)) + if (!VoiceButton.TrySelectId(voiceChoiceId) && + _voiceList.Count > 0 && + VoiceButton.TrySelectId(0)) { - SetVoice(_voiceList[firstVoiceChoiceId].ID); + SetVoice(_voiceList[0].ID); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs` around lines 50 - 55, The code in HumanoidProfileEditor.TTS.cs may throw IndexOutOfRangeException when _voiceList is empty because it accesses _voiceList[firstVoiceChoiceId]; before calling VoiceButton.TrySelectId and SetVoice, guard against an empty list (check _voiceList.Count > 0 or _voiceList.Any()) and only call VoiceButton.TrySelectId(…) or SetVoice(…) when there is at least one element; update the block containing voiceChoiceId, firstVoiceChoiceId, VoiceButton.TrySelectId and SetVoice to skip selection/setting (or use a safe default) when _voiceList is empty.Content.Server/_Art/TTS/TTSSystem.cs-66-72 (1)
66-72:⚠️ Potential issue | 🟡 Minor
async voidбезawaitи отсутствие синхронизации для_ignoredRecipients.
- Метод помечен как
async void, но не содержитawait— модификаторasyncне нужен.- Список
_ignoredRecipientsможет модифицироваться из нескольких сетевых обработчиков без синхронизации.♻️ Предлагаемое исправление
-private async void OnClientOptionTTS(ClientOptionTTSEvent ev, EntitySessionEventArgs args) +private void OnClientOptionTTS(ClientOptionTTSEvent ev, EntitySessionEventArgs args) { if (ev.Enabled) _ignoredRecipients.Remove(args.SenderSession); else _ignoredRecipients.Add(args.SenderSession); }Если требуется потокобезопасность, рассмотрите использование
HashSetс блокировкой илиConcurrentDictionary.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Server/_Art/TTS/TTSSystem.cs` around lines 66 - 72, The handler OnClientOptionTTS is marked async without any await and mutates the shared _ignoredRecipients non-thread-safely; remove the async modifier and make the method synchronous (change signature OnClientOptionTTS to a plain void handler) and protect updates to _ignoredRecipients by either replacing the collection with a concurrent structure (e.g., use a ConcurrentDictionary/ConcurrentHashSet pattern keyed by args.SenderSession) or wrap Add/Remove in a lock using a dedicated private readonly object (e.g., lock(_ignoredRecipientsLock) { ... }); ensure all code paths that modify or read _ignoredRecipients use the chosen synchronization approach.
🧹 Nitpick comments (13)
Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs (1)
1-3: Неиспользуемые директивы using.Импорты
Content.Shared.Speech,Robust.Shared.PrototypesиContent.Shared.Inventoryне используются в этом файле.♻️ Предлагаемое исправление
-using Content.Shared.Speech; -using Robust.Shared.Prototypes; -using Content.Shared.Inventory; - namespace Content.Shared._Art.TTS;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs` around lines 1 - 3, Удалите неиспользуемые директивы using из файла TTSRadioPlayEvent.cs: уберите строки с using Content.Shared.Speech, using Robust.Shared.Prototypes и using Content.Shared.Inventory; оставьте только необходимые пространства имен и сохраните объявление класса/структуры TTSRadioPlayEvent без изменений.Content.Shared/_Art/TTS/TTSVoicePrototype.cs (1)
1-1: Неиспользуемый импортContent.Shared.Humanoid.♻️ Предлагаемое исправление
-using Content.Shared.Humanoid; using Robust.Shared.Prototypes;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/_Art/TTS/TTSVoicePrototype.cs` at line 1, Удалите неиспользуемую директиву использования `using Content.Shared.Humanoid;` из файла — она не применяется в коде класса/файла `TTSVoicePrototype` и должна быть удалена чтобы устранить предупреждение о неиспользуемом импорте.Content.Shared/_Art/CVars/ArtCVars.cs (1)
41-45: Значение по умолчаниюTTSVolume = 0fозначает выключенный звук TTS.Возможно, это намеренно, но пользователи могут не понять, почему TTS не работает. Рассмотрите значение
0.5fили1.0fпо умолчанию.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/_Art/CVars/ArtCVars.cs` around lines 41 - 45, The default TTSVolume CVar is set to 0f which mutes TTS by default; update the CVar default in the TTSVolume declaration (the CVarDef.Create call for TTSVolume) to a sensible audible default such as 0.5f (or 1.0f if you prefer full volume) so users have working TTS out of the box while still allowing them to lower it via the CVar.Content.Server/_Art/TTS/TTSSystem.SSML.cs (1)
21-21: Опечатка в названии:PitchVerylow→PitchVeryLow.Для согласованности с PascalCase рекомендуется переименовать в
PitchVeryLow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Server/_Art/TTS/TTSSystem.SSML.cs` at line 21, Переименуй перечисление-элемент PitchVerylow в PitchVeryLow для соответствия PascalCase: обнови определение enum (в файле TTSSystem.SSML.cs) и все места использования этого значения (ссылки в коде, сравнения, сериализация/десериализация и тесты) так, чтобы вместо PitchVerylow использовалось PitchVeryLow; не забудь также обновить любые строковые представления или документацию, зависящие от имени.Resources/_Art/TTS/tts-voices.yml (1)
1-37: Закомментированный код генератора следует удалить.Закомментированные скрипты Emacs Lisp и Python не должны находиться в файле ресурсов. Переместите их в отдельный каталог
/scriptsили удалите.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Resources/_Art/TTS/tts-voices.yml` around lines 1 - 37, The file contains two commented generator scripts (an Emacs Lisp defun f block and a Python "#!/usr/bin/env python3" voice generator) embedded in Resources/_Art/TTS/tts-voices.yml; remove these commented blocks from the YAML and either delete them or move them into a proper scripts folder (e.g., /scripts) as standalone files (retain the Emacs Lisp function name "f" and the Python generator that reads "voices.json" when moving so they can be located and restored if needed), and ensure the YAML contains only resource data after the change.Content.Shared/VoiceMask/VoiceMaskComponent.cs (1)
18-22: Рассмотрите использованиеProtoId<TTSVoicePrototype>вместоstring.Поле
VoiceIdобъявлено какstring, в то время как вHumanoidProfileComponentаналогичное полеVoiceиспользуетProtoId<TTSVoicePrototype>. Для согласованности типов и типобезопасности при работе с прототипами рекомендуется использоватьProtoId<TTSVoicePrototype>.♻️ Предлагаемое изменение
- [DataField] - [ViewVariables(VVAccess.ReadWrite)] - public string VoiceId = TTSConfig.DefaultVoice; + [DataField] + [ViewVariables(VVAccess.ReadWrite)] + public ProtoId<TTSVoicePrototype> VoiceId = TTSConfig.DefaultVoice;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Shared/VoiceMask/VoiceMaskComponent.cs` around lines 18 - 22, Поле VoiceId в VoiceMaskComponent объявлено как string; замените тип на ProtoId<TTSVoicePrototype> для согласованности с HumanoidProfileComponent::Voice, обновите инициализацию (вместо TTSConfig.DefaultVoice присвойте new ProtoId<TTSVoicePrototype>(TTSConfig.DefaultVoice) или эквивалентный метод-конструктор), добавьте необходимый using/импорт для ProtoId и TTSVoicePrototype и проверьте/скорректируйте все места, где читают/пишут VoiceId (например сравнения или сериализация), чтобы работать с ProtoId<TTSVoicePrototype> вместо string.Content.Server/Radio/EntitySystems/HeadsetSystem.cs (1)
113-123: Дублирование вызова Transform(uid).ParentUid.Переменная
parentуже вычислена на строке 105. На строке 120 повторно вызываетсяTransform(uid).ParentUid. Рекомендуется использовать существующую переменнуюparent.♻️ Предлагаемое исправление
// Art-TTS Start if (TryComp(parent, out ActorComponent? actor)) { _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel); if (args.Voice is string voice) { var ev = new TTSRadioPlayEvent(args.Message, voice, GetNetEntity(uid), GetNetEntity(args.MessageSource)); - RaiseLocalEvent(Transform(uid).ParentUid, ev); + RaiseLocalEvent(parent, ev); } } // Art-TTS End🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Server/Radio/EntitySystems/HeadsetSystem.cs` around lines 113 - 123, The code duplicates Transform(uid).ParentUid even though parent was already computed; inside the TryComp(actor) block where you create the TTSRadioPlayEvent and call RaiseLocalEvent, replace the repeated Transform(uid).ParentUid call with the existing parent variable (computed earlier), keeping the rest of the logic (creating TTSRadioPlayEvent(args.Message, voice, GetNetEntity(uid), GetNetEntity(args.MessageSource)) and calling RaiseLocalEvent(parent, ev)) intact so you no longer recompute the transform.Content.Client/_Art/TTS/ContentAudioSystem.cs (1)
1-8: Документирование "магического числа" TtsMultiplierФайл корректно расположен в
Content.Client/_Art/TTS/, но использует namespaceContent.Client.Audio— это правильный паттерн для partial-классов, так как основное определениеContentAudioSystemнаходится вContent.Client/Audio/.Рекомендуется добавить комментарий для объяснения значения константы
TtsMultiplier = 3f:using Content.Shared.Audio; namespace Content.Client.Audio; public sealed partial class ContentAudioSystem : SharedContentAudioSystem { + /// <summary> + /// Множитель громкости для TTS-аудио для нормализации воспринимаемой громкости. + /// </summary> public const float TtsMultiplier = 3f; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Art/TTS/ContentAudioSystem.cs` around lines 1 - 8, The constant TtsMultiplier = 3f in the partial class ContentAudioSystem is a magic number and should be documented; add a short explanatory comment or XML doc summary above the TtsMultiplier declaration (referencing ContentAudioSystem and TtsMultiplier) that describes what the multiplier means, its units/scale, why the value 3f was chosen, and any implications for audio timing/volume so future maintainers understand its purpose.Content.Client/_Art/TTS/TTSSystem.cs (3)
45-50: Рассмотрите использованиеprivateвместоinternalдля_toDelete.Поле
_toDeleteпомечено какinternal, но используется только внутри этого класса. Рекомендуется использоватьprivateдля лучшей инкапсуляции.♻️ Предлагаемое исправление
-internal List<NetEntity> _toDelete = new(); +private readonly List<NetEntity> _toDelete = new();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Art/TTS/TTSSystem.cs` around lines 45 - 50, The field _toDelete is declared internal but is only used inside this class; change its accessibility to private by renaming its declaration from internal List<NetEntity> _toDelete = new(); to private List<NetEntity> _toDelete = new(); and run a quick search for external references to _toDelete to ensure no external usages break (if any exist, either keep internal or update those call sites).
175-185: Рассмотрите вынесение магического числа3.0fв константу.Множитель
3.0fв расчёте громкости не имеет документации. Рекомендуется вынести его в именованную константу для ясности.+/// <summary> +/// Volume multiplier for TTS audio. +/// </summary> +private const float VolumeMultiplier = 3.0f; + private float AdjustVolume(bool isWhisper) { - var volume = MinimalVolume + SharedAudioSystem.GainToVolume(_volume * 3.0f); + var volume = MinimalVolume + SharedAudioSystem.GainToVolume(_volume * VolumeMultiplier);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Art/TTS/TTSSystem.cs` around lines 175 - 185, В методе AdjustVolume используется "магическое" число 3.0f при вычислении громкости (var volume = MinimalVolume + SharedAudioSystem.GainToVolume(_volume * 3.0f)); вынесите этот множитель в именованную константу (например VolumeGainMultiplier или DefaultGainScale) в области класса рядом с MinimalVolume/WhisperFade, замените 3.0f на константу и добавьте короткий комментарий о назначении константы; проверьте, что AdjustVolume, _volume, SharedAudioSystem.GainToVolume и WhisperFade по-прежнему корректно работают с новой константой.
158-158: Рекомендуется использоватьusingдляMemoryStream.Хотя
MemoryStreamне содержит неуправляемых ресурсов, использованиеusingявляется хорошей практикой дляIDisposableобъектов.♻️ Предлагаемое исправление
-var audioStream = _audioLoader.LoadAudioWav(new MemoryStream(ev.Data)); +using var memoryStream = new MemoryStream(ev.Data); +var audioStream = _audioLoader.LoadAudioWav(memoryStream);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Art/TTS/TTSSystem.cs` at line 158, Объявление MemoryStream при вызове _audioLoader.LoadAudioWav(new MemoryStream(ev.Data)) не обёрнуто в using; оберните создание MemoryStream в using (или используйте using var) чтобы гарантировать Dispose после использования; местоположения для правки: вызов в методе в классе TTSSystem.cs где создаётся переменная audioStream и вызывается метод LoadAudioWav на _audioLoader.Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs (1)
38-48: Упростите логику выбора первого голоса.Переменная
firstVoiceChoiceIdинициализирована значением1, но условиеif (firstVoiceChoiceId == 1)сразу устанавливает её вi(что равно0на первой итерации). Это эквивалентно простому использованию0как индекса по умолчанию.♻️ Предлагаемое упрощение
VoiceButton.Clear(); - var firstVoiceChoiceId = 1; for (var i = 0; i < _voiceList.Count; i++) { var voice = _voiceList[i]; var name = Loc.GetString(voice.Name); VoiceButton.AddItem(name, i); - - if (firstVoiceChoiceId == 1) - firstVoiceChoiceId = i; } var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice); if (!VoiceButton.TrySelectId(voiceChoiceId) && - VoiceButton.TrySelectId(firstVoiceChoiceId)) + VoiceButton.TrySelectId(0)) { - SetVoice(_voiceList[firstVoiceChoiceId].ID); + SetVoice(_voiceList[0].ID); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs` around lines 38 - 48, The logic around firstVoiceChoiceId is redundant: it's initialized to 1 then immediately set to i on the first loop iteration; simplify by initializing firstVoiceChoiceId to 0 (or remove it and use 0 directly) and drop the if check inside the loop. Update the loop over _voiceList (where VoiceButton.AddItem(name, i) is called) to rely on a default index of 0 for the first voice choice and remove the conditional that references firstVoiceChoiceId.Content.Server/_Art/TTS/TTSSystem.cs (1)
74-89: Рекомендуется добавить обработку исключений вasync voidметодах.Методы
async voidне позволяют отслеживать исключения — они могут привести к необработанным ошибкам. Рекомендуется обернуть тело метода вtry-catchс логированием.♻️ Пример обработки исключений
private async void OnRequestPreviewTTS(RequestPreviewTTSEvent ev, EntitySessionEventArgs args) { + try + { if (!_isEnabled || !_prototypeManager.TryIndex<TTSVoicePrototype>(ev.VoiceId, out var protoVoice)) return; if (HandleRateLimit(args.SenderSession) != RateLimitStatus.Allowed) return; var previewText = _rng.Pick(_sampleText); var soundData = await GenerateTTS(previewText, protoVoice.Speaker); if (soundData is null) return; RaiseNetworkEvent(new PlayTTSEvent(soundData, null), Filter.SinglePlayer(args.SenderSession)); + } + catch (Exception ex) + { + Log.Error(ex, "Error processing TTS preview request"); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Content.Server/_Art/TTS/TTSSystem.cs` around lines 74 - 89, OnRequestPreviewTTS is an async void handler so exceptions can escape; wrap the method body in a try/catch that catches Exception (or a more specific set) and logs the error (including context like ev.VoiceId, args.SenderSession) before returning, ensuring failures from GenerateTTS, _prototypeManager.TryIndex, or RaiseNetworkEvent are logged; keep existing early returns (RateLimitStatus check) but put the remaining logic (previewText selection, await GenerateTTS, and RaiseNetworkEvent) inside the try block and log the exception in the catch (include method name OnRequestPreviewTTS and relevant identifiers).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs`:
- Around line 16-20: В коде формирования _voiceList последовательный вызов
.OrderBy() перезаписывает первую сортировку; замените второе .OrderBy(...) на
.ThenBy(...) (или поменяйте порядок вызовов, если нужно сначала по полю Gender,
потом по имени) при перечислении прототипов через _prototypeManager. Убедитесь,
что ссылки на TTSVoicePrototype.Name (через Loc.GetString) и
TTSVoicePrototype.Gender используются в корректных лямбдах для первичной и
вторичной сортировки соответственно.
In `@Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs`:
- Around line 30-34: The current sorting chain on _voices uses two OrderBy calls
so the second replaces the first; change the second OrderBy to ThenBy so the
list is primarily ordered by Loc.GetString(o.Name) and secondarily by the gender
expression — update the LINQ chain that starts with
_proto.EnumeratePrototypes<TTSVoicePrototype>() and the two OrderBy(...) calls
(the one ordering by name and the one ordering by the gender bitmask) to use
ThenBy for the secondary sort.
- Around line 24-29: Подписка на VoiceSelector.OnItemSelected внутри
ReloadVoices() приводит к множественным обработчикам при повторных вызовах;
переместите подписку в конструктор класса или гарантируйте идемпотентность
(например, отписаться перед новой подпиской или проверять, что обработчик ещё не
добавлен) и оставьте внутри обработчика вызовы VoiceSelector.SelectId(args.Id) и
проверку VoiceSelector.SelectedMetadata с вызовом
OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata).
In `@Content.Server.Database/Model.cs`:
- Line 334: Добавлено не-nullable свойство Profile.Voice, но нет миграции EF для
добавления соответствующего столбца в БД; создайте и примените миграцию Entity
Framework которая добавляет столбец Voice в таблицу профилей (либо сделать
свойство nullable и миграцию для nullable-столбца, либо добавить non-nullable
столбец с подходящим значением по умолчанию), затем выполнить обновление БД
(например через «dotnet ef migrations add» и «dotnet ef database update») чтобы
схема БД соответствовала модели Profile.Voice.
In `@Content.Server/_Art/TTS/TTSManager.cs`:
- Around line 40-42: The deduplication is not thread-safe: replace the
Dictionary<string, SemaphoreSlim> _semaphores usage with a ConcurrentDictionary
and use GetOrAdd (or ConcurrentDictionary<string, SemaphoreSlim>.GetOrAdd) to
ensure a single SemaphoreSlim per cacheKey atomically (fix the current
_semaphores.GetValueOrDefault(...) + _semaphores[cacheKey] = ... race). Protect
mutations of _cache and _cacheKeysSeq by either switching to concurrent
collections (e.g., ConcurrentDictionary for _cache and a thread-safe structure
for ordering) or by serializing access with a private lock object so the
add/evict sequence is atomic (fix parallel modifications at the
_cache/_cacheKeysSeq block). When cleaning up the semaphore in the finally
block, only remove/dispose it if you successfully removed the exact instance
(use ConcurrentDictionary.TryRemove with the expected instance) so you don't
race with a new requester. Update ResetCache() to also clear/ dispose
_semaphores to avoid inconsistent state. Finally, ensure CancellationTokenSource
and HttpResponseMessage are disposed (use using statements or explicit Dispose
in finally) where they are created.
- Around line 92-113: Обратите внимание, что CancellationTokenSource (cts) и
HttpResponseMessage (response) не освобождаются при ранних return — оберните
создание cts и получение response в using/using var (или try/finally с
.Dispose()) чтобы гарантировать вызов Dispose() при любом выходе; конкретно
измените создание и использование CancellationTokenSource и вызов
_httpClient.GetAsync(...) (переменные cts и response в TTSManager.cs) так, чтобы
response был в using-блоке и чтение response.Content.ReadAsByteArrayAsync()
происходило внутри этого блока перед возвратом данных, а при ошибочных кодах
сразу возвращали null после автоматического освобождения ресурсов.
In `@Content.Server/_Art/TTS/TTSSystem.cs`:
- Around line 150-155: The code computes a linear distance into variable
distance via .Length() but compares it to SharedChatSystem.VoiceRange *
SharedChatSystem.VoiceRange (a squared value), causing incorrect range checks;
fix by making the comparisons consistent: either compute squared distance (use
the vector delta's LengthSquared or equivalent) and compare to
VoiceRange*VoiceRange and WhisperClearRange*WhisperClearRange, or keep distance
as linear and compare to VoiceRange and WhisperClearRange directly; update the
checks around xformQuery/GetWorldPosition -> distance and the RaiseNetworkEvent
call (obfTtsEvent vs fullTtsEvent) accordingly so both comparisons use the same
metric.
In `@Content.Server/_Art/TTS/TTSSystem.Sanitize.cs`:
- Around line 311-321: The declension call in NumberToText is using (int)(value
% 10) which loses the tens place and causes wrong forms for 11–14; update the
call in NumberToText to pass (int)(value % 100) to GetDeclension so the function
can correctly detect 11–14 exceptions (modify NumberToText where it currently
calls GetDeclension and replace the modulus 10 with modulus 100).
- Around line 27-29: Сейчас порядок вызовов делает правила замены из Latin
(например ключи "id", "gps", "c4") недостижимыми, потому что предыдущие вызовы
(например Dygraphs.Replace и MatchedWord.Replace) уже изменяют текст;
переместите вызов Latin.Replace(text, ReplaceLat2Cyr) так, чтобы он выполнялся
до тех вызовов, которые превращают латинские последовательности в кириллические
(то есть вызывать Latin.Replace перед Dygraphs.Replace и MatchedWord.Replace), и
оставьте ReplaceLat2Cyr/ReplaceMatchedWord как есть.
In `@Content.Server/_Art/TTS/TTSSystem.SSML.cs`:
- Around line 6-14: The ToSsmlText method embeds the raw text into SSML without
escaping XML-special characters; update it to escape the input before wrapping
with prosody/speak (e.g., use System.Security.SecurityElement.Escape or create
an XText to produce a safe string) so '<', '>', '&', '"' and '\'' are encoded;
apply the escape to the initial text variable (used by ToSsmlText) and then
preserve the existing SoundTraits checks (RateFast, PitchVerylow) and prosody
wrapping logic so the escaped text is what gets inserted into the <prosody> and
final <speak> output.
In `@Content.Server/_Art/TTS/VoiceMaskSystem.TTS.cs`:
- Around line 16-19: В обработчике OnSpeakerVoiceTransform для
Entity<VoiceMaskComponent> не затирайте уже установленный args.Args.VoiceId
пустым значением маски; перед присвоением ent.Comp.VoiceId проверяйте, что само
значение действительно задано (не null/пустая строка/значение по умолчанию) и
только тогда заменяйте args.Args.VoiceId; иначе пропустите присвоение, чтобы
сохранить исходный голос (ссылки: OnSpeakerVoiceTransform, VoiceMaskComponent,
InventoryRelayedEvent<TransformSpeakerVoiceEvent>, args.Args.VoiceId,
ent.Comp.VoiceId).
In `@Content.Shared/Humanoid/HumanoidProfileComponent.cs`:
- Around line 28-32: Поле Voice в HumanoidProfileComponent помечено только
[DataField("voice")] и потому не синхронизируется; добавьте атрибут
AutoNetworkedField рядом с DataField (например [DataField("voice"),
AutoNetworkedField]) к объявлению public ProtoId<TTSVoicePrototype> Voice =
TTSConfig.DefaultVoice; чтобы поведение соответствовало другим полям (Gender,
Sex, Age, Species) и значение голоса реплицировалось клиентам.
In `@Content.Shared/Humanoid/HumanoidProfileExportV1.cs`:
- Around line 65-67: В V1 DTO (HumanoidProfileExportV1) добавлено поле Voice, но
ToV2() сейчас всегда прокидывает его дальше — нужно добавить явную
проверку/фолбек: в методе ToV2() (и аналогично в месте на строках ~89-91)
определить, валиден ли ProtoId<TTSVoicePrototype> Voice (не равен
default/пустому/невалидному), и только тогда записывать его в V2-профиль; если
значение отсутствует — либо не устанавливать поле в V2, либо подставлять
допустимый дефолт (например null/отсутствующий идентификатор) чтобы не собирать
профиль с невалидным голосом. Упомяните конкретно поле Voice и метод ToV2() при
правке.
In `@Content.Shared/Preferences/HumanoidCharacterProfile.cs`:
- Around line 38-40: MemberwiseEquals and GetHashCode currently ignore the new
Voice property, so update both methods to include Voice in their comparisons and
hash computation: inside MemberwiseEquals (the method that compares two
HumanoidCharacterProfile instances) add a comparison for Voice (e.g.,
string.Equals(this.Voice, other.Voice, StringComparison.Ordinal)) alongside the
existing field comparisons, and in GetHashCode include Voice when combining
hashes (the same hash-combining logic used for other fields) so changes to Voice
affect equality and the resulting hash; also ensure the same change is applied
where similar logic exists around the region referenced (lines ~321-324) to keep
all equality/hash logic consistent.
---
Minor comments:
In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs`:
- Around line 50-55: The code in HumanoidProfileEditor.TTS.cs may throw
IndexOutOfRangeException when _voiceList is empty because it accesses
_voiceList[firstVoiceChoiceId]; before calling VoiceButton.TrySelectId and
SetVoice, guard against an empty list (check _voiceList.Count > 0 or
_voiceList.Any()) and only call VoiceButton.TrySelectId(…) or SetVoice(…) when
there is at least one element; update the block containing voiceChoiceId,
firstVoiceChoiceId, VoiceButton.TrySelectId and SetVoice to skip
selection/setting (or use a safe default) when _voiceList is empty.
In `@Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs`:
- Around line 196-200: The UpdateTTSVoicesControls method can be called when TTS
is disabled which leaves _voiceList empty and yields an inconsistent UI; either
add a guard before calling it in SetProfile and OnReset (check
configurationManager.GetCVar(ArtCVars.TTSClientEnabled) or TTSContainer.Visible)
or change UpdateTTSVoicesControls to be a safe no-op when TTS is off (early
return if TTS disabled or if _voiceList is null/empty) so it does not rely on
InitializeVoice having run; update references to InitializeVoice, SetProfile,
OnReset, _voiceList, and VoiceButton accordingly.
In `@Content.Client/Options/UI/Tabs/AudioTab.xaml`:
- Line 11: В XAML убери жёстко закодированные русские строки и замени их на
локализуемые ресурсы, используя шаблон {Loc 'KeyName'} для элементов
SliderVolumeTts (Title) и соответствующего чекбокса/элемента "Включить ТТС"
(примерно на строке с именем, содержащим Tts или EnableTts); добавь уникальные
ключи (например Audio.VolumeTts и Audio.EnableTts) в файлы локализации и обнови
ресурсы перевода, чтобы сохранить консистентность с остальными UI-элементами.
In `@Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs`:
- Around line 90-92: Reset the selector before attempting to re-select the voice
so stale UI state is cleared: set the VoiceSelector selection to an
empty/fallback state (e.g., SelectedIndex = -1 or clear selection) prior to
calling _voices.FindIndex(...), then if voiceIdx != -1 call
VoiceSelector.Select(voiceIdx); ensure you reference the existing
variables/methods (_voices, voiceIdx, VoiceSelector.Select) and add the explicit
reset when the voice is not found so the UI shows a cleared/fallback choice
instead of the previous selection.
In `@Content.Server/_Art/TTS/TTSSystem.cs`:
- Around line 66-72: The handler OnClientOptionTTS is marked async without any
await and mutates the shared _ignoredRecipients non-thread-safely; remove the
async modifier and make the method synchronous (change signature
OnClientOptionTTS to a plain void handler) and protect updates to
_ignoredRecipients by either replacing the collection with a concurrent
structure (e.g., use a ConcurrentDictionary/ConcurrentHashSet pattern keyed by
args.SenderSession) or wrap Add/Remove in a lock using a dedicated private
readonly object (e.g., lock(_ignoredRecipientsLock) { ... }); ensure all code
paths that modify or read _ignoredRecipients use the chosen synchronization
approach.
In `@Content.Shared/_Art/CVars/ArtCVars.cs`:
- Around line 17-21: Комментарий XML над объявлением public static readonly
CVarDef<bool> TTSClientEnabled некорректен (копипаст — "URL of the TTS server
API"); замените его на корректное описание состояния этой булевой переменной,
например кратко поясняющее, что переменная включает/отключает TTS-клиент
(например: "Enable/disable the TTS client" или на русском "Включает/выключает
TTS-клиент"), сохранив формат <summary>... </summary> над TTSClientEnabled и не
меняя саму сигнатуру CVarDef<bool>.
In `@Resources/_Art/TTS/tts-voices.yml`:
- Around line 93-112: The YAML currently lists synthetic TTS voice identifiers
for "Джо Байден" (speaker/id: biden), "Барак Обама" (speaker/id: obama) and
"Дональд Трамп" (speaker/id: trump) without clarifying they are
synthetic/parody; add a brief comment near these entries (or at the top of
Resources/_Art/TTS/tts-voices.yml) stating that these are synthetic TTS voices
generated from text (not recorded audio), intended for parody/entertainment in a
non-commercial/game context and not representations of real recorded
performances, to reduce legal ambiguity and clarify intent.
---
Nitpick comments:
In `@Content.Client/_Art/TTS/ContentAudioSystem.cs`:
- Around line 1-8: The constant TtsMultiplier = 3f in the partial class
ContentAudioSystem is a magic number and should be documented; add a short
explanatory comment or XML doc summary above the TtsMultiplier declaration
(referencing ContentAudioSystem and TtsMultiplier) that describes what the
multiplier means, its units/scale, why the value 3f was chosen, and any
implications for audio timing/volume so future maintainers understand its
purpose.
In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs`:
- Around line 38-48: The logic around firstVoiceChoiceId is redundant: it's
initialized to 1 then immediately set to i on the first loop iteration; simplify
by initializing firstVoiceChoiceId to 0 (or remove it and use 0 directly) and
drop the if check inside the loop. Update the loop over _voiceList (where
VoiceButton.AddItem(name, i) is called) to rely on a default index of 0 for the
first voice choice and remove the conditional that references
firstVoiceChoiceId.
In `@Content.Client/_Art/TTS/TTSSystem.cs`:
- Around line 45-50: The field _toDelete is declared internal but is only used
inside this class; change its accessibility to private by renaming its
declaration from internal List<NetEntity> _toDelete = new(); to private
List<NetEntity> _toDelete = new(); and run a quick search for external
references to _toDelete to ensure no external usages break (if any exist, either
keep internal or update those call sites).
- Around line 175-185: В методе AdjustVolume используется "магическое" число
3.0f при вычислении громкости (var volume = MinimalVolume +
SharedAudioSystem.GainToVolume(_volume * 3.0f)); вынесите этот множитель в
именованную константу (например VolumeGainMultiplier или DefaultGainScale) в
области класса рядом с MinimalVolume/WhisperFade, замените 3.0f на константу и
добавьте короткий комментарий о назначении константы; проверьте, что
AdjustVolume, _volume, SharedAudioSystem.GainToVolume и WhisperFade по-прежнему
корректно работают с новой константой.
- Line 158: Объявление MemoryStream при вызове _audioLoader.LoadAudioWav(new
MemoryStream(ev.Data)) не обёрнуто в using; оберните создание MemoryStream в
using (или используйте using var) чтобы гарантировать Dispose после
использования; местоположения для правки: вызов в методе в классе TTSSystem.cs
где создаётся переменная audioStream и вызывается метод LoadAudioWav на
_audioLoader.
In `@Content.Server/_Art/TTS/TTSSystem.cs`:
- Around line 74-89: OnRequestPreviewTTS is an async void handler so exceptions
can escape; wrap the method body in a try/catch that catches Exception (or a
more specific set) and logs the error (including context like ev.VoiceId,
args.SenderSession) before returning, ensuring failures from GenerateTTS,
_prototypeManager.TryIndex, or RaiseNetworkEvent are logged; keep existing early
returns (RateLimitStatus check) but put the remaining logic (previewText
selection, await GenerateTTS, and RaiseNetworkEvent) inside the try block and
log the exception in the catch (include method name OnRequestPreviewTTS and
relevant identifiers).
In `@Content.Server/_Art/TTS/TTSSystem.SSML.cs`:
- Line 21: Переименуй перечисление-элемент PitchVerylow в PitchVeryLow для
соответствия PascalCase: обнови определение enum (в файле TTSSystem.SSML.cs) и
все места использования этого значения (ссылки в коде, сравнения,
сериализация/десериализация и тесты) так, чтобы вместо PitchVerylow
использовалось PitchVeryLow; не забудь также обновить любые строковые
представления или документацию, зависящие от имени.
In `@Content.Server/Radio/EntitySystems/HeadsetSystem.cs`:
- Around line 113-123: The code duplicates Transform(uid).ParentUid even though
parent was already computed; inside the TryComp(actor) block where you create
the TTSRadioPlayEvent and call RaiseLocalEvent, replace the repeated
Transform(uid).ParentUid call with the existing parent variable (computed
earlier), keeping the rest of the logic (creating
TTSRadioPlayEvent(args.Message, voice, GetNetEntity(uid),
GetNetEntity(args.MessageSource)) and calling RaiseLocalEvent(parent, ev))
intact so you no longer recompute the transform.
In `@Content.Shared/_Art/CVars/ArtCVars.cs`:
- Around line 41-45: The default TTSVolume CVar is set to 0f which mutes TTS by
default; update the CVar default in the TTSVolume declaration (the
CVarDef.Create call for TTSVolume) to a sensible audible default such as 0.5f
(or 1.0f if you prefer full volume) so users have working TTS out of the box
while still allowing them to lower it via the CVar.
In `@Content.Shared/_Art/TTS/TTSRadioPlayEvent.cs`:
- Around line 1-3: Удалите неиспользуемые директивы using из файла
TTSRadioPlayEvent.cs: уберите строки с using Content.Shared.Speech, using
Robust.Shared.Prototypes и using Content.Shared.Inventory; оставьте только
необходимые пространства имен и сохраните объявление класса/структуры
TTSRadioPlayEvent без изменений.
In `@Content.Shared/_Art/TTS/TTSVoicePrototype.cs`:
- Line 1: Удалите неиспользуемую директиву использования `using
Content.Shared.Humanoid;` из файла — она не применяется в коде класса/файла
`TTSVoicePrototype` и должна быть удалена чтобы устранить предупреждение о
неиспользуемом импорте.
In `@Content.Shared/VoiceMask/VoiceMaskComponent.cs`:
- Around line 18-22: Поле VoiceId в VoiceMaskComponent объявлено как string;
замените тип на ProtoId<TTSVoicePrototype> для согласованности с
HumanoidProfileComponent::Voice, обновите инициализацию (вместо
TTSConfig.DefaultVoice присвойте new
ProtoId<TTSVoicePrototype>(TTSConfig.DefaultVoice) или эквивалентный
метод-конструктор), добавьте необходимый using/импорт для ProtoId и
TTSVoicePrototype и проверьте/скорректируйте все места, где читают/пишут VoiceId
(например сравнения или сериализация), чтобы работать с
ProtoId<TTSVoicePrototype> вместо string.
In `@Resources/_Art/TTS/tts-voices.yml`:
- Around line 1-37: The file contains two commented generator scripts (an Emacs
Lisp defun f block and a Python "#!/usr/bin/env python3" voice generator)
embedded in Resources/_Art/TTS/tts-voices.yml; remove these commented blocks
from the YAML and either delete them or move them into a proper scripts folder
(e.g., /scripts) as standalone files (retain the Emacs Lisp function name "f"
and the Python generator that reads "voices.json" when moving so they can be
located and restored if needed), and ensure the YAML contains only resource data
after the change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: b8468225-fb6c-42f9-956d-f7d627809cf0
📒 Files selected for processing (43)
Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.csContent.Client/Lobby/UI/HumanoidProfileEditor.xamlContent.Client/Lobby/UI/HumanoidProfileEditor.xaml.csContent.Client/Options/UI/Tabs/AudioTab.xamlContent.Client/Options/UI/Tabs/AudioTab.xaml.csContent.Client/VoiceMask/VoiceMaskBoundUserInterface.csContent.Client/VoiceMask/VoiceMaskNameChangeWindow.xamlContent.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.csContent.Client/_Art/TTS/ContentAudioSystem.csContent.Client/_Art/TTS/HumanoidProfileEditor.TTS.csContent.Client/_Art/TTS/TTSSystem.csContent.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.csContent.Server.Database/Model.csContent.Server/Database/ServerDbBase.csContent.Server/Entry/EntryPoint.csContent.Server/IoC/ServerContentIoC.csContent.Server/Preferences/Managers/ServerPreferencesManager.csContent.Server/Radio/EntitySystems/HeadsetSystem.csContent.Server/Radio/EntitySystems/RadioSystem.csContent.Server/Radio/RadioEvent.csContent.Server/VoiceMask/VoiceMaskSystem.csContent.Server/_Art/TTS/TTSManager.csContent.Server/_Art/TTS/TTSSystem.RateLimit.csContent.Server/_Art/TTS/TTSSystem.SSML.csContent.Server/_Art/TTS/TTSSystem.Sanitize.csContent.Server/_Art/TTS/TTSSystem.csContent.Server/_Art/TTS/VoiceMaskSystem.TTS.csContent.Shared/Humanoid/HumanoidProfileComponent.csContent.Shared/Humanoid/HumanoidProfileExportV1.csContent.Shared/Inventory/InventorySystem.Relay.csContent.Shared/Preferences/HumanoidCharacterProfile.csContent.Shared/VoiceMask/SharedVoiceMaskSystem.csContent.Shared/VoiceMask/VoiceMaskComponent.csContent.Shared/_Art/CVars/ArtCVars.csContent.Shared/_Art/TTS/ClientOptionTTSEvent.csContent.Shared/_Art/TTS/PlayTTSEvent.csContent.Shared/_Art/TTS/RequestPreviewTTSEvent.csContent.Shared/_Art/TTS/SharedVoiceMaskSystem.csContent.Shared/_Art/TTS/TTSComponent.csContent.Shared/_Art/TTS/TTSConfig.csContent.Shared/_Art/TTS/TTSRadioPlayEvent.csContent.Shared/_Art/TTS/TTSVoicePrototype.csResources/_Art/TTS/tts-voices.yml
| _voiceList = _prototypeManager | ||
| .EnumeratePrototypes<TTSVoicePrototype>() | ||
| .OrderBy(o => Loc.GetString(o.Name)) | ||
| .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0)) | ||
| .ToList(); |
There was a problem hiding this comment.
Ошибка сортировки: второй OrderBy заменяет первый.
Использование двух последовательных .OrderBy() приводит к тому, что вторая сортировка полностью заменяет первую. Для стабильной многоуровневой сортировки следует использовать .ThenBy().
🐛 Предлагаемое исправление
_voiceList = _prototypeManager
.EnumeratePrototypes<TTSVoicePrototype>()
.OrderBy(o => Loc.GetString(o.Name))
- .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
+ .ThenBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
.ToList();Примечание: Если цель — сначала сортировать по полу, а затем по имени, порядок вызовов нужно поменять местами.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| _voiceList = _prototypeManager | |
| .EnumeratePrototypes<TTSVoicePrototype>() | |
| .OrderBy(o => Loc.GetString(o.Name)) | |
| .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0)) | |
| .ToList(); | |
| _voiceList = _prototypeManager | |
| .EnumeratePrototypes<TTSVoicePrototype>() | |
| .OrderBy(o => Loc.GetString(o.Name)) | |
| .ThenBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0)) | |
| .ToList(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs` around lines 16 - 20, В
коде формирования _voiceList последовательный вызов .OrderBy() перезаписывает
первую сортировку; замените второе .OrderBy(...) на .ThenBy(...) (или поменяйте
порядок вызовов, если нужно сначала по полю Gender, потом по имени) при
перечислении прототипов через _prototypeManager. Убедитесь, что ссылки на
TTSVoicePrototype.Name (через Loc.GetString) и TTSVoicePrototype.Gender
используются в корректных лямбдах для первичной и вторичной сортировки
соответственно.
| VoiceSelector.OnItemSelected += args => | ||
| { | ||
| VoiceSelector.SelectId(args.Id); | ||
| if (VoiceSelector.SelectedMetadata != null) | ||
| OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata); | ||
| }; |
There was a problem hiding this comment.
Повторная подписка на событие при каждом вызове ReloadVoices().
Подписка на VoiceSelector.OnItemSelected происходит внутри ReloadVoices(). При повторных вызовах метода обработчик будет добавлен многократно, что приведёт к многократным вызовам OnVoiceChange.
🐛 Предлагаемое исправление
Перенесите подписку в конструктор или добавьте проверку:
+private bool _eventSubscribed;
+
private void ReloadVoices()
{
if (_cfg is null)
return;
TTSContainer.Visible = _cfg.GetCVar(ArtCVars.TTSClientEnabled);
if (!_cfg.GetCVar(ArtCVars.TTSClientEnabled))
return;
- VoiceSelector.OnItemSelected += args =>
- {
- VoiceSelector.SelectId(args.Id);
- if (VoiceSelector.SelectedMetadata != null)
- OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata);
- };
+ if (!_eventSubscribed)
+ {
+ VoiceSelector.OnItemSelected += args =>
+ {
+ VoiceSelector.SelectId(args.Id);
+ if (VoiceSelector.SelectedMetadata != null)
+ OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata);
+ };
+ _eventSubscribed = true;
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs` around lines 24 -
29, Подписка на VoiceSelector.OnItemSelected внутри ReloadVoices() приводит к
множественным обработчикам при повторных вызовах; переместите подписку в
конструктор класса или гарантируйте идемпотентность (например, отписаться перед
новой подпиской или проверять, что обработчик ещё не добавлен) и оставьте внутри
обработчика вызовы VoiceSelector.SelectId(args.Id) и проверку
VoiceSelector.SelectedMetadata с вызовом
OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata).
| _voices = _proto | ||
| .EnumeratePrototypes<TTSVoicePrototype>() | ||
| .OrderBy(o => Loc.GetString(o.Name)) | ||
| .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0)) | ||
| .ToList(); |
There was a problem hiding this comment.
Неправильная цепочка сортировки: OrderBy().OrderBy() вместо OrderBy().ThenBy().
Второй вызов OrderBy() полностью заменяет первую сортировку вместо вторичной сортировки. Используйте ThenBy() для сохранения первичной сортировки по имени.
🐛 Предлагаемое исправление
_voices = _proto
.EnumeratePrototypes<TTSVoicePrototype>()
- .OrderBy(o => Loc.GetString(o.Name))
- .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
+ .OrderBy(o => ((o.Gender == "male") ? 1 : 0) + ((o.Gender == "female") ? 2 : 0))
+ .ThenBy(o => Loc.GetString(o.Name))
.ToList();📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| _voices = _proto | |
| .EnumeratePrototypes<TTSVoicePrototype>() | |
| .OrderBy(o => Loc.GetString(o.Name)) | |
| .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0)) | |
| .ToList(); | |
| _voices = _proto | |
| .EnumeratePrototypes<TTSVoicePrototype>() | |
| .OrderBy(o => ((o.Gender == "male") ? 1 : 0) + ((o.Gender == "female") ? 2 : 0)) | |
| .ThenBy(o => Loc.GetString(o.Name)) | |
| .ToList(); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs` around lines 30 -
34, The current sorting chain on _voices uses two OrderBy calls so the second
replaces the first; change the second OrderBy to ThenBy so the list is primarily
ordered by Loc.GetString(o.Name) and secondarily by the gender expression —
update the LINQ chain that starts with
_proto.EnumeratePrototypes<TTSVoicePrototype>() and the two OrderBy(...) calls
(the one ordering by name and the one ordering by the gender bitmask) to use
ThenBy for the secondary sort.
| public string Sex { get; set; } = null!; | ||
| public string Gender { get; set; } = null!; | ||
| public string Species { get; set; } = null!; | ||
| public string Voice { get; set; } = null!; // Art-TTS |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Поиск миграций, связанных с Voice или TTS
fd -e cs . | xargs rg -l -i "voice|tts" | head -20
rg -n "Voice" --type cs -g "*Migration*"Repository: ss14-art/open-space
Length of output: 1149
🏁 Script executed:
# Find migration files and directories
fd -t d "Migrations" --hidden
find . -type f -name "*Migration*.cs" | head -20
find . -type f -path "*/Migrations/*.cs" | head -30Repository: ss14-art/open-space
Length of output: 2582
🏁 Script executed:
# Check the Model.cs file to confirm Voice property
head -360 Content.Server.Database/Model.cs | tail -40Repository: ss14-art/open-space
Length of output: 1799
🏁 Script executed:
# Search for any recent migration files mentioning database schema changes
fd -t f -e cs . -path "*/Migrations/*" | xargs ls -lt 2>/dev/null | head -20Repository: ss14-art/open-space
Length of output: 1509
🏁 Script executed:
# Find the most recent migration files
ls -lat Content.Server.Database/Migrations/Postgres/ | head -30Repository: ss14-art/open-space
Length of output: 2574
🏁 Script executed:
# Check the latest migration file to see what columns it adds
tail -100 "$(ls -t Content.Server.Database/Migrations/Postgres/*.cs | grep -v Designer | head -1)"Repository: ss14-art/open-space
Length of output: 4658
🏁 Script executed:
# Search for "Voice" in any migration files to see if it's mentioned
rg "Voice" Content.Server.Database/Migrations/Postgres/Repository: ss14-art/open-space
Length of output: 45
Требуется миграция базы данных для нового столбца Voice.
Добавление не-nullable свойства Voice в модель Profile требует соответствующей миграции Entity Framework. Проверка показала, что ни один файл миграции не содержит упоминания о столбце Voice, что приведёт к несоответствию схемы базы данных модели C#. Без миграции существующие записи профилей будут вызывать ошибки при попытке чтения, так как столбец отсутствует в базе данных.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Server.Database/Model.cs` at line 334, Добавлено не-nullable свойство
Profile.Voice, но нет миграции EF для добавления соответствующего столбца в БД;
создайте и примените миграцию Entity Framework которая добавляет столбец Voice в
таблицу профилей (либо сделать свойство nullable и миграцию для
nullable-столбца, либо добавить non-nullable столбец с подходящим значением по
умолчанию), затем выполнить обновление БД (например через «dotnet ef migrations
add» и «dotnet ef database update») чтобы схема БД соответствовала модели
Profile.Voice.
| private readonly Dictionary<string, byte[]> _cache = new(); | ||
| private readonly Dictionary<string, SemaphoreSlim> _semaphores = new(); | ||
| private readonly List<string> _cacheKeysSeq = new(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and read the TTSManager.cs file
fd TTSManager.cs --type fRepository: ss14-art/open-space
Length of output: 100
🏁 Script executed:
# Read the TTSManager.cs file to understand the code structure
cat -n Content.Server/_Art/TTS/TTSManager.csRepository: ss14-art/open-space
Length of output: 9249
🏁 Script executed:
# Verify if there are any lock mechanisms or thread-safe collections in the class
rg -n "lock|Monitor|ReaderWriterLock|ConcurrentDictionary|Interlocked" Content.Server/_Art/TTS/TTSManager.csRepository: ss14-art/open-space
Length of output: 45
🏁 Script executed:
# Check if there's a using statement or disposal pattern for HttpResponseMessage or CancellationTokenSource
rg -n "using.*HttpResponseMessage|using.*CancellationTokenSource|response\.Dispose|cts\.Dispose" Content.Server/_Art/TTS/Repository: ss14-art/open-space
Length of output: 45
Эта дедупликация не потокобезопасна.
Строки 75-76: _semaphores.GetValueOrDefault(...) и _semaphores[cacheKey] = ... не атомарны. Два одновременных запроса для одного cacheKey могут создать разные SemaphoreSlim — оба ждут на своих объектах и независимо уходят в API, что нарушает дедупликацию.
Строки 117-123: Параллельные модификации _cache и _cacheKeysSeq без синхронизации вызывают гонки.
Строки 146-147: Удаление семафора в finally перед завершением — новый запрос может создать новый семафор, пока предыдущий ещё в полёте.
Строки 151-155: ResetCache() не очищает _semaphores, что создаёт состояние рассогласованности.
Дополнительно, строка 93 (CancellationTokenSource) и строка 98 (HttpResponseMessage) не освобождаются, оба реализуют IDisposable.
Also applies to: 75-76, 117-123, 146-147, 151-155
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Server/_Art/TTS/TTSManager.cs` around lines 40 - 42, The
deduplication is not thread-safe: replace the Dictionary<string, SemaphoreSlim>
_semaphores usage with a ConcurrentDictionary and use GetOrAdd (or
ConcurrentDictionary<string, SemaphoreSlim>.GetOrAdd) to ensure a single
SemaphoreSlim per cacheKey atomically (fix the current
_semaphores.GetValueOrDefault(...) + _semaphores[cacheKey] = ... race). Protect
mutations of _cache and _cacheKeysSeq by either switching to concurrent
collections (e.g., ConcurrentDictionary for _cache and a thread-safe structure
for ordering) or by serializing access with a private lock object so the
add/evict sequence is atomic (fix parallel modifications at the
_cache/_cacheKeysSeq block). When cleaning up the semaphore in the finally
block, only remove/dispose it if you successfully removed the exact instance
(use ConcurrentDictionary.TryRemove with the expected instance) so you don't
race with a new requester. Update ResetCache() to also clear/ dispose
_semaphores to avoid inconsistent state. Finally, ensure CancellationTokenSource
and HttpResponseMessage are disposed (use using statements or explicit Dispose
in finally) where they are created.
| private string ToSsmlText(string text, SoundTraits traits = SoundTraits.None) | ||
| { | ||
| var result = text; | ||
| if (traits.HasFlag(SoundTraits.RateFast)) | ||
| result = $"<prosody rate=\"fast\">{result}</prosody>"; | ||
| if (traits.HasFlag(SoundTraits.PitchVerylow)) | ||
| result = $"<prosody pitch=\"x-low\">{result}</prosody>"; | ||
| return $"<speak>{result}</speak>"; | ||
| } |
There was a problem hiding this comment.
Отсутствует экранирование XML-символов во входном тексте.
Параметр text напрямую встраивается в SSML XML без экранирования специальных символов (<, >, &, ", '). Если пользовательский текст содержит эти символы, результат будет невалидным SSML или потенциально уязвимым для инъекции.
🐛 Предлагаемое исправление с экранированием
+using System.Security;
+
private string ToSsmlText(string text, SoundTraits traits = SoundTraits.None)
{
- var result = text;
+ var result = SecurityElement.Escape(text);
if (traits.HasFlag(SoundTraits.RateFast))
result = $"<prosody rate=\"fast\">{result}</prosody>";
if (traits.HasFlag(SoundTraits.PitchVerylow))
result = $"<prosody pitch=\"x-low\">{result}</prosody>";
return $"<speak>{result}</speak>";
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Server/_Art/TTS/TTSSystem.SSML.cs` around lines 6 - 14, The
ToSsmlText method embeds the raw text into SSML without escaping XML-special
characters; update it to escape the input before wrapping with prosody/speak
(e.g., use System.Security.SecurityElement.Escape or create an XText to produce
a safe string) so '<', '>', '&', '"' and '\'' are encoded; apply the escape to
the initial text variable (used by ToSsmlText) and then preserve the existing
SoundTraits checks (RateFast, PitchVerylow) and prosody wrapping logic so the
escaped text is what gets inserted into the <prosody> and final <speak> output.
| private void OnSpeakerVoiceTransform(Entity<VoiceMaskComponent> ent, ref InventoryRelayedEvent<TransformSpeakerVoiceEvent> args) | ||
| { | ||
| args.Args.VoiceId = ent.Comp.VoiceId; | ||
| } |
There was a problem hiding this comment.
Не затирайте исходный голос пустым значением маски.
Если у маски VoiceId не выбран, обработчик все равно перезаписывает уже вычисленный args.Args.VoiceId. В итоге надетая маска без собственного TTS-голоса сбрасывает голос персонажа вместо сохранения исходного.
💡 Предлагаемое исправление
private void OnSpeakerVoiceTransform(Entity<VoiceMaskComponent> ent, ref InventoryRelayedEvent<TransformSpeakerVoiceEvent> args)
{
- args.Args.VoiceId = ent.Comp.VoiceId;
+ if (!string.IsNullOrEmpty(ent.Comp.VoiceId))
+ args.Args.VoiceId = ent.Comp.VoiceId;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private void OnSpeakerVoiceTransform(Entity<VoiceMaskComponent> ent, ref InventoryRelayedEvent<TransformSpeakerVoiceEvent> args) | |
| { | |
| args.Args.VoiceId = ent.Comp.VoiceId; | |
| } | |
| private void OnSpeakerVoiceTransform(Entity<VoiceMaskComponent> ent, ref InventoryRelayedEvent<TransformSpeakerVoiceEvent> args) | |
| { | |
| if (!string.IsNullOrEmpty(ent.Comp.VoiceId)) | |
| args.Args.VoiceId = ent.Comp.VoiceId; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Server/_Art/TTS/VoiceMaskSystem.TTS.cs` around lines 16 - 19, В
обработчике OnSpeakerVoiceTransform для Entity<VoiceMaskComponent> не затирайте
уже установленный args.Args.VoiceId пустым значением маски; перед присвоением
ent.Comp.VoiceId проверяйте, что само значение действительно задано (не
null/пустая строка/значение по умолчанию) и только тогда заменяйте
args.Args.VoiceId; иначе пропустите присвоение, чтобы сохранить исходный голос
(ссылки: OnSpeakerVoiceTransform, VoiceMaskComponent,
InventoryRelayedEvent<TransformSpeakerVoiceEvent>, args.Args.VoiceId,
ent.Comp.VoiceId).
|
|
||
| // Art-TTS Start | ||
| [DataField("voice")] | ||
| public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile | ||
| // Art-TTS End |
There was a problem hiding this comment.
Отсутствует атрибут AutoNetworkedField для поля Voice.
Поле Voice имеет только [DataField("voice")], но не имеет [AutoNetworkedField]. Все остальные поля в этом компоненте (Gender, Sex, Age, Species) используют оба атрибута [DataField, AutoNetworkedField]. Без AutoNetworkedField значение голоса не будет синхронизироваться с клиентами.
🐛 Предлагаемое исправление
// Art-TTS Start
- [DataField("voice")]
+ [DataField("voice"), AutoNetworkedField]
public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile
// Art-TTS End📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Art-TTS Start | |
| [DataField("voice")] | |
| public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile | |
| // Art-TTS End | |
| // Art-TTS Start | |
| [DataField("voice"), AutoNetworkedField] | |
| public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; // HumanoidCharacterProfile | |
| // Art-TTS End |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Shared/Humanoid/HumanoidProfileComponent.cs` around lines 28 - 32,
Поле Voice в HumanoidProfileComponent помечено только [DataField("voice")] и
потому не синхронизируется; добавьте атрибут AutoNetworkedField рядом с
DataField (например [DataField("voice"), AutoNetworkedField]) к объявлению
public ProtoId<TTSVoicePrototype> Voice = TTSConfig.DefaultVoice; чтобы
поведение соответствовало другим полям (Gender, Sex, Age, Species) и значение
голоса реплицировалось клиентам.
| // Art-TTS Start | ||
| [DataField] | ||
| public ProtoId<TTSVoicePrototype> Voice; |
There was a problem hiding this comment.
Не ломайте импорт старых V1-профилей.
В DTO версии 1 появилось новое поле Voice, а ToV2() теперь безусловно прокидывает его дальше. Старые экспортированные V1-файлы этого поля не содержат, поэтому сюда придёт default ProtoId, и миграция соберёт профиль с невалидным голосом. Нужен явный fallback для отсутствующего значения либо отдельная версия формата.
Also applies to: 89-91
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Shared/Humanoid/HumanoidProfileExportV1.cs` around lines 65 - 67, В
V1 DTO (HumanoidProfileExportV1) добавлено поле Voice, но ToV2() сейчас всегда
прокидывает его дальше — нужно добавить явную проверку/фолбек: в методе ToV2()
(и аналогично в месте на строках ~89-91) определить, валиден ли
ProtoId<TTSVoicePrototype> Voice (не равен default/пустому/невалидному), и
только тогда записывать его в V2-профиль; если значение отсутствует — либо не
устанавливать поле в V2, либо подставлять допустимый дефолт (например
null/отсутствующий идентификатор) чтобы не собирать профиль с невалидным
голосом. Упомяните конкретно поле Voice и метод ToV2() при правке.
| // Art-TTS Start | ||
| [DataField] | ||
| public string Voice { get; set; } = TTSConfig.DefaultVoice; |
There was a problem hiding this comment.
Voice нужно включить в сравнение и хеш профиля.
После добавления этого поля MemberwiseEquals() и GetHashCode() ниже по файлу все еще его игнорируют. Изменение только голоса тогда выглядит как отсутствие изменений и может не сохраниться, а Equals/GetHashCode перестают описывать фактическое состояние профиля.
💡 Что нужно добавить ниже по файлу
public bool MemberwiseEquals(HumanoidCharacterProfile other)
{
if (Name != other.Name) return false;
if (Age != other.Age) return false;
if (Sex != other.Sex) return false;
if (Gender != other.Gender) return false;
if (Species != other.Species) return false;
if (PreferenceUnavailable != other.PreferenceUnavailable) return false;
if (SpawnPriority != other.SpawnPriority) return false;
if (!_jobPriorities.SequenceEqual(other._jobPriorities)) return false;
if (!_antagPreferences.SequenceEqual(other._antagPreferences)) return false;
if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false;
if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
if (FlavorText != other.FlavorText) return false;
+ if (Voice != other.Voice) return false;
return Appearance.Equals(other.Appearance);
}
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(_jobPriorities);
hashCode.Add(_antagPreferences);
hashCode.Add(_traitPreferences);
hashCode.Add(_loadouts);
hashCode.Add(Name);
hashCode.Add(FlavorText);
+ hashCode.Add(Voice);
hashCode.Add(Species);
hashCode.Add(Age);Also applies to: 321-324
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Content.Shared/Preferences/HumanoidCharacterProfile.cs` around lines 38 - 40,
MemberwiseEquals and GetHashCode currently ignore the new Voice property, so
update both methods to include Voice in their comparisons and hash computation:
inside MemberwiseEquals (the method that compares two HumanoidCharacterProfile
instances) add a comparison for Voice (e.g., string.Equals(this.Voice,
other.Voice, StringComparison.Ordinal)) alongside the existing field
comparisons, and in GetHashCode include Voice when combining hashes (the same
hash-combining logic used for other fields) so changes to Voice affect equality
and the resulting hash; also ensure the same change is applied where similar
logic exists around the region referenced (lines ~321-324) to keep all
equality/hash logic consistent.
|
@cryals сделай миграцию пожалуйста, я уже закончу остальное. Буду тестить уже самостоятельно, если всё не полетит как обычно. Пока что буду заниматься другими задачами. |
Краткое описание
Почему мы должны добавить это?
Медиа (Видео/Скриншоты)
Проверочный пункт
Changelog
🆑 GqXgji, Al-S
Summary by CodeRabbit
Новые функции